#!/usr/bin/env python
# Create: 2018/8/11
__author__ = '749B'

from django import template
from django.utils.safestring import mark_safe
from django.utils import timezone


register = template.Library()


@register.simple_tag
def my_debug(r):
    """留着调试用"""
    print('debug', r)
    return ""


@register.simple_tag
def print_str(o):
    """打印obj
    这个方法貌似是因为models的__str__方法没有别写好，才能解决其中的部分问题
    要保证所有model的str方法的返回值都是字符串
    """
    return o.__str__()


@register.filter
def get_filed_verbose(field_name, meta):
    """通过字段名，获取字段的verbose_name"""
    return meta.get_field(field_name).verbose_name


@register.simple_tag
def get_meta(model):
    """因为前段不能用下划线，只能来这里处理后返回"""
    return model._meta


@register.simple_tag
def errors_get_all(errors):
    """因为前段不能用下划线，只能来这里处理后返回"""
    return errors.get('__all__')


@register.filter
def get_attr(o, name):
    return getattr(o, name, '')


@register.simple_tag
def get_filter_field(field_name, admin_obj, request_get):
    """返回给前端一个下拉框
    只能给有choices选项的字段设置list_filter，否则这里会报错
    """
    field_obj = admin_obj.model._meta.get_field(field_name)
    select_ele = '<select class="form-control" name="%s" onchange="window.location.href=this.value">' % field_name
    for choice_item in field_obj.get_choices(blank_choice=[('0', '全部')]):
        selected = 'selected' if request_get.get(field_name) == str(choice_item[0]) else ''
        select_ele += '<option %s value="%s">%s</option>' % (
            selected,
            href_change(field_name, choice_item[0], request_get),  # 调用另一个方法，生成url的get请求参数
            choice_item[1]
        )
    select_ele += '</select>'
    return mark_safe(select_ele)


@register.simple_tag
def href_changes(keys, values, request_get):
    """修改或添加get请求的多个参数"""
    get_dic = request_get.copy()
    for key, value in zip(keys, values):
        if value == '0' and key in get_dic:
            del get_dic[key]
        else:
            get_dic[key] = value
    search_list = []
    for k, v in get_dic.items():
        search_list.append('%s=%s' % (k, v))
    search = '&'.join(search_list)
    return '?%s' % search if search else ''


@register.simple_tag
def href_change(k, v, request_get):
    """如果只有一对参数，就用这个方法。主要是前端转不了元组"""
    return href_changes((k,), (v,), request_get)


@register.simple_tag
def render_page_eles(objs, pages, request_get):
    """前端的页码
    一次生成所有的页码
    并且逻辑上非常好的实现了固定显示多少页（pages），头尾的情况都没问题
    这里要调用上面的 href_change(page, request_get) 来生成href的内容
    """
    anchor = '#page-navigation'  # 给所有a标签的最后加上锚点
    page_range = objs.paginator.page_range
    page = objs.number
    eles = ['<li class="active"><a href="%s%s">%s</a></li>' % (href_change('page', page, request_get), anchor, page)]
    pages = pages-1 if isinstance(pages, int) and pages else 2  # 每次生成一个标签，就自减1。这是判断下变量的类型
    skip = 1
    while pages:
        flag = True
        if objs.number+skip in page_range:
            page = objs.number + skip
            eles.append('<li><a href="%s%s">%s</a></li>' % (href_change('page', page, request_get), anchor, page))
            flag = False
            pages -= 1
        if pages and objs.number-skip in page_range:
            page = objs.number - skip
            eles.insert(0, '<li><a href="%s%s">%s</a></li>' % (href_change('page', page, request_get), anchor, page))
            flag = False
            pages -= 1
        if flag:
            break
        skip += 1
    return mark_safe(''.join(eles))


@register.simple_tag
def render_th_eles(admin_obj, request_get):
    """生成带排序功能的th标签
    事件不能直接写在标签里，我试下来无法阻止事件冒泡
    只有表里的字段能排序
    """
    """下面是样例，不过实现方法不再是在标签里写onclick事件了，而是去js里做事件绑定
    <th onclick="location='{% get_order_href forloop.counter request.GET %}'">
        {{ field_name|get_filed_verbose:meta }}
        <div class="btn-group btn-group-xs pull-right" role="group" aria-label="...">
            <button type="button" class="btn btn-default">
                <span title="删除排序" class="glyphicon glyphicon-sort" aria-hidden="true"></span>
            </button>
            <span class="badge btn-group">2</span>
            <button type="button" class="btn btn-default">
                <span title="升降序切换" class="glyphicon glyphicon-menu-down" aria-hidden="true"></span>
            </button>
        </div>
    </th>
    """
    ele_list = []
    loop_counter = 1
    for field in admin_obj.list_display:
        if callable(field):
            # 支持按steed_admin里定义的方法显示，方法要写在类外面
            field_title = getattr(field, 'short_description', field.__name__)
            eles = ['<th>%s</th>' % field_title]
        elif hasattr(admin_obj, field):
            # 支持按steed_admin的定义的方法名显示，方法要写在类里面
            func = getattr(admin_obj, field)
            field_title = getattr(func, 'short_description', func.__name__)
            eles = ['<th>%s</th>' % field_title]
        elif callable(getattr(admin_obj.model, field)):
            # 支持在model里定义方法，显示
            func = getattr(admin_obj.model, field)
            field_title = getattr(func, 'short_description', None)
            eles = ['<th>%s</th>' % field_title]
        else:
            field_title = get_filed_verbose(field, admin_obj.model._meta)
            # 排序是用ORM的order_by做的，所以只有表里的字段才有排序
            o = request_get.get('o')
            if o:
                o = o.join('.' * 2)
                loop_counter_p = '%s'.join('.'*2) % loop_counter
                loop_counter_n = '-%s'.join('.'*2) % loop_counter
                href_add = ''
                if loop_counter_p in o:
                    href_del = o.replace(loop_counter_p, '.', 1)
                    href_rev = o.replace(loop_counter_p, loop_counter_n, 1)
                    if o.startswith(loop_counter_p):  # 如果当前字段已经是第一个排序字段，那么点击效果是反转
                        href_add = href_rev
                    badge_index = o.split('.').index(str(loop_counter))
                    icon_class = 'glyphicon glyphicon-menu-up'
                elif loop_counter_n in o:
                    href_del = o.replace(loop_counter_n, '.', 1)
                    href_rev = o.replace(loop_counter_n, loop_counter_p, 1)
                    if o.startswith(loop_counter_n):
                        href_add = href_rev
                    badge_index = o.split('.').index('-%s' % str(loop_counter))
                    icon_class = 'glyphicon glyphicon-menu-down'
                else:
                    href_del = o
                    href_rev = None
                    badge_index = None
                    icon_class = None
                href_add = href_add or '.%s%s' % (loop_counter, href_del)
                eles = ['<th href="%s">' % href_change('o', href_add.strip('.'), request_get),
                        field_title,
                        ]
                if href_rev:
                    eles.append('<div class="btn-group btn-group-xs pull-right" role="group" aria-label="...">')
                    eles.append('<button type="button" class="btn btn-default" href="%s">' %
                                href_change('o', href_del.strip('.'), request_get))
                    eles.append('<span title="删除排序" class="glyphicon glyphicon-sort" aria-hidden="true"></span>')
                    eles.append('</button>')
                    eles.append('<span class="badge btn-group">%s</span>' % badge_index)
                    eles.append('<button type="button" class="btn btn-default" href="%s">' %
                                href_change('o', href_rev.strip('.'), request_get))
                    eles.append('<span title="升降序切换" class="%s" aria-hidden="true"></span>' % icon_class)
                    eles.append('</button>')
                    eles.append('</div>')
                eles.append('</th>')
            else:
                eles = ['<th href="%s">' % href_change('o', loop_counter, request_get),
                        field_title,
                        '</th>',
                        ]
        ele_list.append(''.join(eles))
        loop_counter += 1

    return mark_safe(''.join(ele_list))


@register.simple_tag
def render_td_eles(obj, admin_obj, request_path):
    """返回表格的td
    for循环里的内容是，返回记录的值
    通过反射，根据字段名，获取记录里的值
    然后做一下判断，做一些输出的格式化：
    1. 有choices字段，返回choices的内容
    2. 时间格式，转为本时区的时间并做格式化
    """
    ele_list = []
    for field in admin_obj.list_display:
        eles = ['<td>']
        if not ele_list:
            eles.append('<a href="%s%s/change/">' % (request_path, obj.id))

        if callable(field):
            # # 支持按steed_admin里定义的方法显示，方法要写在类外面
            v = field(obj)
        elif hasattr(admin_obj, field):
            # 支持按steed_admin的定义的方法名显示，方法要写在类里面
            func = getattr(admin_obj, field)
            v = func(obj)
        elif callable(getattr(obj, field)):
            # 支持在model里定义方法，显示
            func = getattr(obj, field)
            v = func()
        else:
            field_obj = obj._meta.get_field(field)
            # 记录是否有choices，只有在实例化后的记录里确认，models的类里是找不到的
            # 因为你定义的时候，choice属性是只有在实例化的时候在__init__()构造方法里传入的
            if field_obj.choices:
                v = getattr(obj, 'get_%s_display' % field)()
            else:
                v = getattr(obj, field)
            # 检查返回数据的类型，如果是时间类型，就做下转化。日期类型没遇到，就先不做了
            if type(v).__name__ == 'datetime':
                v = timezone.localtime(v)  # 转为当前时区的时间
                v = v.strftime("%Y-%m-%d %H:%M:%S")  # 格式化输出
        eles.append(str(v))

        if not ele_list:
            eles.append('</a>')
        eles.append('</td>')
        ele_list.append(''.join(eles))
    return mark_safe(''.join(ele_list))


@register.simple_tag
def get_field_long(field):
    """判断输入框的类型，返回栅格系统的长度"""
    dic = {
        'default': 1,
        'TextInput': 3,
        'NumberInput': 2,
        'Select': 2,
        'SelectMultiple': 2,
        'Textarea': 5,
    }
    field_name = field.field.widget.__class__.__name__
    if field_name in dic:
        return dic[field_name]
    else:
        return dic['default']


@register.simple_tag
def related_objs_lookup(objs):
    """返回关联信息的ul标签
    总结一下逻辑：
    0. 正向关联的一对多，包括一对一，不用管
    1. 把正向关联的多对多列出来，不判断重复的问题
    2. 列出所有的反向关联
    3. 处理反向的各种关联，这里要考虑重复的问题（多对多依然不考虑重复），只在第一次出现的时候列出
    3.1 反向的一对多关联，需要继续下钻，对查出来的对象做递归
    3.2 反向的多对多关联，不需要继续下钻。实现的时候还是调用递归，但是不处理关联和反向关联。注意不考虑重复问题
    3.3 另外两种反向关联没确认，默认先按照反向的一对多处理。

    和 django admin 的区别：
    多对多正向以及反向有区别
    我这里直接找到关联的记录显示出来，但是其实多对多删除的时候，并不影响关联的记录。最终我在名字前加了个'_'来标记
    django admin 列出的是第三张表里的对应的记录，貌似这个逻辑是对的
    所以多对多是不需要递归的，反向多对多不需要下钻

    最终我领悟到的逻辑：
    列出多对多或反向多对多的第三张表的记录
    列出反向一对多、一对一，对应的记录
    并且对这条列出的反向记录再应用上面的2条，即递归。
    """

    added = set()  # 这个集合用来判断，重复的项目只在第一次列出来

    def recursive(objs):
        """这是一个递归的方法"""
        ele = "<ul>"
        for obj in objs:
            if obj not in added:
                added.add(obj)
                ele += "<li>%s: %s</li>" % (obj._meta.verbose_name, obj)

            # 一对多的关联都是记录到本表里的，所以不用罗列

            # 先处理多对多的关联，不用考虑重复，用不到集合
            many_to_many = obj._meta.many_to_many  # 这个对象的所有多对多关联
            # print(many_to_many)
            for m2m in many_to_many:
                m2m_objs = getattr(obj, m2m.name).all()
                through = getattr(obj, m2m.name).through
                if len(m2m_objs) > 0:
                    sub_ele = "<ul>"
                    for o in m2m_objs:
                        kwargs = {obj._meta.model_name: obj, o._meta.model_name: o}
                        sub_ele += "<li>%s: %s %s 关联 %s</li>" % (
                            through._meta.verbose_name, through.objects.filter(**kwargs)[0], m2m.verbose_name, o)
                    sub_ele += '</ul>'
                    ele += sub_ele

            # 最后处理反向的关联，应该都是一个逻辑
            # 这里需要检查是否重复，重复项只在第一次出现
            related_objs = obj._meta.related_objects  # 所有关联到这个对象的，有 ManyToOneRel 和 ManyToManyRel
            # print(related_objs)
            for rel in related_objs:
                accessor_obj = getattr(obj, rel.get_accessor_name())
                # print(accessor_obj.all())
                target_objs = accessor_obj.all()
                if len(target_objs) > 0:
                    if rel.one_to_many:  # 这个是 ManyToOneRel，需要做递归
                        pass
                    elif rel.one_to_one:  # 这个分支还没试，但是应该和上面的一样
                        pass
                    elif rel.many_to_many:  # 这个是 ManyToManyRel
                        # 是反向的多对多。直接处理，也不用继续下钻，所以最后continue了
                        ele += "<ul>"
                        for target_obj in target_objs:
                            through = rel.through
                            kwargs = {obj._meta.model_name: obj, target_obj._meta.model_name: target_obj}
                            ele += "<li>%s: %s 关联 %s: %s</li>" % (
                                through._meta.verbose_name, through.objects.filter(**kwargs)[0],
                                target_obj, rel.field.verbose_name)
                        ele += "</ul>"
                        continue
                    elif rel.many_to_one:  # 这个分支应该不存在
                        pass
                    nodes = recursive(target_objs)
                    ele += nodes
        ele += "</ul>"
        return ele

    return mark_safe(recursive(objs))


@register.simple_tag
def get_m2m_lists(form_obj, field_name):
    """返回多对多的两个列表"""
    if form_obj.instance.id is not None:
        selected_objs = getattr(form_obj.instance, field_name).all()
    else:
        selected_objs = []
    # print(selected_objs)
    all_objs = form_obj.fields[field_name].queryset
    # print(all_objs)
    rest_objs = list(set(all_objs) - set(selected_objs))
    # print(rest_objs)
    return {'selected': selected_objs, 'rest': rest_objs}





